<?php

namespace W3;

/**
 *
 * 框架验证类
 *
 * @author edikud
 * @date 2022/10/22
 * @copyright Copyright (c) 2022 W3 (http://www.mcooo.com)
 * @license GNU General Public License 2.0
 */
class Validate
{
    /**
     * 内部数据
     *
     * @access private
     * @var array
     */
    private $data;

    /**
     * 当前验证指针
     *
     * @access private
     * @var string
     */
    private $key;

    /**
     * 验证规则数组
     *
     * @access private
     * @var array
     */
    private $rules = [];

    /**
     * 中断模式,一旦出现验证错误即抛出而不再继续执行
     *
     * @access private
     * @var boolean
     */
    private $break = false;

    /**
     * 验证方法数组
     *
     * @var array
     */
    private $methods = [];

    /**
     * 单例句柄
     *
     * @access protected
     * @var Validate
     */
    protected static $instance;
	
    /**
     * 单例实例
     *
     * @return Validate
     */
    public static function instance(): Validate
    {
        if (null === self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * 创建新实例
     *
     * @access public
     * @return Validate
     */
    public static function make(): Validate
    {
        return new static();
    }

    /**
     * Retrieves the default validators
     *
     * @return void
     */
    public function __construct()
    {
		$this->methods['required'] =  [$this, 'required'];
		$this->methods['confirm'] =  [$this, 'confirm'];
		
		$methods = [
		    'regex', 'in', 'range', 'max', 
		    'min', 'enum', 'email', 'url', 
			'alpha', 'alnum', 'float', 'number', 
			'int', 'ip', 'date', 'time', 'string', 'xss'
		];
		$class = Validate::class;
        foreach ($methods as $method)
		{
			$this->methods[$method] = [$class, $method];
        }
    }
	
    /**
     * 注册验证器
     *
     * @param string   $method   validation method
     * @param callable $callable validation callable
     *
     * @return void
     */
    public function register(string $method, callable $callable)
    {
        $this->methods[$method] = $callable;
    }
	
    /**
     * 增加验证规则
     *
     * @access public
     * @param string $key 数值键值
     * @param string $rule 规则名称
     * @param string $message 错误字符串
     * @return Validation
     */
    public function addRule(string $key, ...$params)
    {
		$this->rules[$key][] = $params;
        return $this;
    }

    /**
     * 设置为中断模式
     *
     * @access public
     * @return void
     */
    public function setBreak()
    {
        $this->break = true;
		return $this;
    }

    /**
     * Run the Validator
     * This function does all the work.
     *
     * @access	public
     * @param   array $data 需要验证的数据
     * @param   array $rules 验证数据遵循的规则
     * @return	array
     * @throws  Exception
     */
    public function run(array $data, $rules = NULL)
    {
        $result = [];
        $this->data = $data;
        $rules = empty($rules) ? $this->rules : $rules;

        // Cycle through the rules and test for errors
        foreach ($rules as $key => $rules) 
		{
            $this->key = $key;
            $data[$key] = (is_array($data[$key]) ? 0 == count($data[$key])
                : 0 == strlen($data[$key] ?? '')) ? NULL : $data[$key];

            foreach ($rules as $params) 
			{
                $method = $params[0];

                if ('required' != $method && 'confirm' != $method && 0 == strlen($data[$key] ?? '')) {
                    continue;
                }

                is_string($method) && isset($this->methods[$method]) && $method = $this->methods[$method];

                $message = $params[1];
                $params[1] = $data[$key];
                $params = array_slice($params, 1);
				
                if (!call_user_func_array($method, $params)) {
                    $result[$key] = $message;
                    break;
                }
            }

            /** 开启中断 */
            if ($this->break && $result) {
                break;
            }
        }

        return $result;
    }
	
    /**
     * 验证输入是否一致
     *
     * @access public
     * @param string $str 待处理的字符串
     * @param string $key 需要一致性检查的键值
     * @return boolean
     */
    public function confirm(?string $str, string $key): bool
    {
        return !empty($this->data[$key]) ? ($str == $this->data[$key]) : empty($str);
    }

    /**
     * 是否为空
     *
     * @access public
     * @return boolean
     */
    public function required(): bool
    {
        return !empty($this->data[$this->key]);
    }
	

    /**
     * 匹配正则表达式
     *
     * @param string $value
     * @param string $regex
     * @return boolean
     */
    public static function regex(string $value, string $regex): bool
    {
        return preg_match($regex, $value) ? true : false;
    }

    /**
     * 检查是否在数组中
     *
     * @param mixed $value
     * @param array $list
     * @return boolean
     */
    public static function in(string $str, array $params): bool
    {
        return in_array($str, $params);
    }

    /**
     * 范围 $range: array(38, 130)
     *
     * @param mixed $value numbernic|string
     * @param array $range
     * @return boolean
     */
    public static function range(string $value, array $range): bool
    {
        is_string($value) && $value = strlen($value);
		return (($value >= $range[0]) && ($value <= $range[1]));
    }

    /**
     * 最大长度
     *
     * @param mixed $str
     * @param integer $length
     * @return boolean
     */
    public static function max(string $str, int $length): bool
    {
        return (Util::length($str) < $length);
    }

    /**
     * 最小长度
     *
     * @param mixed $str
     * @param integer $length
     * @return boolean
     */
    public static function min(string $str, int $length): bool
    {
        return (Util::length($str) >= $length);
    }

    /**
     * 枚举类型判断
     *
     * @param string $str 待处理的字符串
     * @param array $params 枚举值
     * @return unknown
     */
    public static function enum(string $str, array $params): bool
    {
		$keys = array_flip($params);
        return isset($keys[$str]);
    }

    /**
     * 检查电子邮件
     *
     * @param string $str
     * @return boolean
     */
    public static function email(string $str): bool
    {
		return preg_match("/^[_a-z0-9-\.]+@([-a-z0-9]+\.)+[a-z]{2,}$/i", $str);
    }

    /**
     * 验证是否为网址
     *
     * @param string $str
     * @return boolean
     */
    public static function url(string $str): bool
    {
        $parts = @parse_url($str);
        if (!$parts) {
            return false;
        }

        return isset($parts['scheme']) &&
            in_array($parts['scheme'], ['http', 'https', 'ftp']) &&
            !preg_match('/(\(|\)|\\\|"|<|>|[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x19])/', $str);
    }

    /**
     * Alpha
     *
     * @param string $str
     * @return boolean
     */
    public static function alpha(string $str): bool
    {
		return preg_match("/^([a-z])+$/i", $str) ? true : false;
    }
	
    /**
     * Alpha-numeric
     *
     * @param string $str
     * @return boolean
     */
    public static function alnum(string $str): bool
    {
		return preg_match("/^([a-z0-9])+$/i", $str);
    }
	
    /**
     * float
     *
     * @param string $str
     * @return boolean
     */
    public static function float(string $str): bool
    {
		return preg_match("/^[0-9\.]+$/", $str);
    }
	
    /**
     * 是否为数字或数字字符串
     *
     * @param string $str
     * @return boolean
     */
    public static function number(string $str): bool
    {
		return is_numeric($str);
    }
	
    /**
     * 是否是正整数
     *
     * @param string $str
     * @return boolean
     */
    public static function int(string $str): bool
    {
		return (bool) preg_match('/^\d+$/i',$str);
        //return is_int($str) || ctype_digit($str);
    }	
	
    /**
     * 检查是否为ip
     *
     * @param string $str
     * @return boolean
     */
    public static function ip(string $str): bool
    {
		return ((false !== ip2long($str)) && (long2ip(ip2long($str)) === $str));
    }	
	
    /**
     * 检查是否是日期
     *
     * @param string $str
     * @return boolean
     */
    public static function date(string $str): bool
    {
		return preg_match('/^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/', $str) ? true : false;
    }	
	
    /**
     * 检查是否为日期时间
     *
     * @param string $datetime
     * @return boolean
     */
    public static function time(string $datetime, string $format = 'Y-m-d H:i:s'): bool
    {
		return ($time = strtotime($datetime)) && ($datetime == date($format, $time));
    }
	
    /**
     * 检查是否为字符串
     *
     * @param string $str
     * @return boolean
     */
    public static function string(string $str): bool
    {
		return is_string($str);
    }
	
    /**
     * 对xss字符串的检测
     *
     * @access public
     * @param string $str
     * @return boolean
     */
    public static function xss(string $str): bool
    {
        $search = 'abcdefghijklmnopqrstuvwxyz';
        $search .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
        $search .= '1234567890!@#$%^&*()';
        $search .= '~`";:?+/={}[]-_|\'\\';

        for ($i = 0; $i < strlen($search); $i++) {
            // ;? matches the ;, which is optional
            // 0{0,7} matches any padded zeros, which are optional and go up to 8 chars

            // &#x0040 @ search for the hex values
            $str = preg_replace('/(&#[xX]0{0,8}'.dechex(ord($search[$i])).';?)/i', $search[$i], $str); // with a ;
            // &#00064 @ 0{0,7} matches '0' zero to seven times
            $str = preg_replace('/(&#0{0,8}'.ord($search[$i]).';?)/', $search[$i], $str); // with a ;
        }

        return !preg_match('/(\(|\)|\\\|"|<|>|[\x00-\x08]|[\x0b-\x0c]|[\x0e-\x19]|' . "\r|\n|\t" . ')/', $str);
    }
}
